/**
 * Copyright (c) 2008, George Taylor
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, are permitted 
 * provided that the following conditions are met:
 * 
 *   * Redistributions of source code must retain the above copyright notice, this list of 
 *     conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright notice, this list of 
 *     conditions and the following disclaimer in the documentation and/or other materials 
 *     provided with the distribution.
 *   * Neither the name of the copyright holder's organization nor the names of its contributors 
 *     may be used to endorse or promote products derived from this software without specific 
 *     prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 
 * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package uk.co.taylorconsulting.annoweb.web.builder;

import java.io.File;
import java.io.FileOutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;

import uk.co.taylorconsulting.annoweb.annotation.action.Parameter;
import uk.co.taylorconsulting.annoweb.annotation.form.Clean;
import uk.co.taylorconsulting.annoweb.annotation.form.Form;
import uk.co.taylorconsulting.annoweb.annotation.form.validation.Required;
import uk.co.taylorconsulting.annoweb.annotation.injection.DefaultCacheValue;
import uk.co.taylorconsulting.annoweb.annotation.injection.DefaultValue;
import uk.co.taylorconsulting.annoweb.annotation.injection.Inject;
import uk.co.taylorconsulting.annoweb.annotation.injection.Injectable;
import uk.co.taylorconsulting.annoweb.annotation.injection.MultipleInjection;
import uk.co.taylorconsulting.annoweb.annotation.security.Role;
import uk.co.taylorconsulting.annoweb.annotation.web.DefaultForward;
import uk.co.taylorconsulting.annoweb.annotation.web.Forward;
import uk.co.taylorconsulting.annoweb.annotation.web.GlobalForward;
import uk.co.taylorconsulting.annoweb.web.builder.data.ActionHolder;
import uk.co.taylorconsulting.annoweb.web.builder.data.FormData;
import uk.co.taylorconsulting.annoweb.web.builder.data.Globals;
import uk.co.taylorconsulting.annoweb.web.builder.data.Site;
import uk.co.taylorconsulting.annoweb.web.builder.data.SiteHolder;
import uk.co.taylorconsulting.annoweb.web.resources.Messages;

public class SiteBuilder {
	private static final Logger      LOG      = Logger.getLogger(SiteBuilder.class);
	private static final SiteBuilder instance = new SiteBuilder();
	
	public static void buildSite(Site s, File tmpLocation, HashMap<String, SiteHolder> methods) {
		instance.buildSite(s, tmpLocation, methods, true);
	}
	
	public void buildSite(Site s, File tmpLocation, HashMap<String, SiteHolder> methods, boolean ok) {
		File siteFile = new File(tmpLocation, "Site" + removeSpace(s.getName()) + ".java");
		StringBuffer str = addClassStart(s);
		addMethods(s, str, methods);
		str.append("}\n");
		FileOutputStream fos;
		try {
			fos = new FileOutputStream(siteFile);
			fos.write(str.toString().getBytes());
			fos.close();
		} catch (Exception e) {
			LOG.warn("Error writing class to " + siteFile.getAbsolutePath() + "(" + e.getMessage() + ")");
		}
	}
	
	private void addMethods(Site s, StringBuffer str, HashMap<String, SiteHolder> methods) {
		ArrayList<String> compMethods = new ArrayList<String>();
		for (ActionHolder ah : s.getActions().values()) {
			if (!compMethods.contains(ah.getMethod().getClass().getCanonicalName() + ":" + ah.getMethod().getName())) {
				compMethods.add(ah.getMethod().getClass().getCanonicalName() + ":" + ah.getMethod().getName());
				str.append("\tpublic void process").append(ah.getMethod().getName()).append("(HttpServletRequest request, HttpServletResponse response, ServletContext servletContext) {\n");
				str.append("\t\t").append(ah.getMethod().getDeclaringClass().getCanonicalName()).append(" actionClass= (").append(ah.getMethod().getDeclaringClass().getCanonicalName()).append(")getCache(\"").append(
				        ah.getMethod().getDeclaringClass().getCanonicalName()).append("\");\n");
				str.append("\t\tif(actionClass != null) {\n");
				str.append("\t\t\ttry {\n");
				Method m = ah.getMethod();
				Class<?> clazz = m.getDeclaringClass();
				if ((s.getRoles().get(clazz) != null && s.getRoles().get(clazz).size() > 0) || ah.getRoles().size() > 0) {
					str.append("\t\t\t\tif (SecurityHandler.checkSecurity(request, response, servletContext, ");
					ArrayList<String> roles = new ArrayList<String>();
					boolean first = true;
					first = addRoles(str, roles, first, s.getRoles().get(clazz));
					addRoles(str, roles, first, ah.getRoles());
					str.append(")) {\n");
				}
				if (ah.getMethod().getReturnType() == String.class) {
					str.append("\t\t\t\tString retValue = actionClass.");
				} else {
					str.append("\t\t\t\tactionClass.");
				}
				str.append(ah.getMethod().getName()).append("(").append(buildMethodSigniture(ah.getMethod(), ah.getMethod().getAnnotation(Required.class), false, s.getGlobals())).append(");\n");
				if (ah.getMethod().getReturnType() == String.class) {
					// test value to forward
					boolean first = true;
					for (Forward f : ah.getForwards()) {
						if (first) {
							first = false;
							str.append("\t\t\t\t");
						} else {
							str.append("\t\t\t\t} else ");
						}
						str.append("if (\"").append(f.name()).append("\".equals(retValue)) {\n");
						addForward(str, f.path(), f.web(), f.method(), f.redirect(), "\t\t\t\t\t", s, null);
					}
					ArrayList<Forward> sForwards = s.getForward(ah.getMethod().getDeclaringClass());
					if (sForwards != null) {
						for (Forward f : sForwards) {
							if (first) {
								first = false;
								str.append("\t\t\t\t");
							} else {
								str.append("\t\t\t\t} else ");
							}
							str.append("if (\"").append(f.name()).append("\".equals(retValue)) {\n");
							addForward(str, f.path(), f.web(), f.method(), f.redirect(), "\t\t\t\t\t", s, null);
						}
					}
					if (first) {
						str.append("\t\t\t\t");
						first = false;
					} else {
						str.append("\t\t\t\t} else ");
					}
					str.append("if (!GlobalDataHandler.globalForward(retValue, request, response, servletContext)) {\n");
					// do default forwards
					addDefaultForward(s, str, ah, "\t\t\t\t\t");
					str.append("\t\t\t\t}\n");
				} else {
					// send to default forward
					addDefaultForward(s, str, ah, "\t\t\t\t");
				}
				if (s.getRoles().size() > 0 || ah.getRoles().size() > 0) {
					str.append("\t\t\t\t}\n");
				}
				str.append("\t\t\t} catch (Throwable t) {\n");
				str.append("\t\t\t\tGlobalDataHandler.handleException(t, request, response, servletContext);\n");
				str.append("\t\t\t}\n");
				str.append("\t\t}\n");
				str.append("\t}\n");
			}
		}
	}
	
	private boolean addRoles(StringBuffer str, ArrayList<String> roles, boolean first, ArrayList<Role> rT) {
		if (rT != null && rT.size() > 0) {
			for (Role r : rT) {
				if (!roles.contains(r.value())) {
					roles.add(r.value());
					if (first) {
						first = false;
					} else {
						str.append(",");
					}
					str.append("\"").append(r.value()).append("\"");
				}
			}
		}
		return first;
	}
	
	protected void addDefaultForward(Site s, StringBuffer str, ActionHolder ah, String indent) {
		String defF = ah.getDefaultForward() == null ? null : ah.getDefaultForward().value();
		if (defF == null) {
			if (ah.getMethod() != null) {
				DefaultForward df = s.getDefaultForwards().get(ah.getMethod().getDeclaringClass());
				if (df != null) {
					defF = df.value();
				}
			}
		}
		Forward f = null;
		if (defF == null) {
			if (s.getGlobals().getDefaultForward() != null) {
				defF = s.getGlobals().getDefaultForward().value();
			}
		} else {
			f = ah.getForward(defF);
			if (f == null) {
				if (ah.getMethod() != null) {
					for (Forward fTmp : s.getForward(ah.getMethod().getDeclaringClass())) {
						if (fTmp.name().equals(defF)) {
							f = fTmp;
							break;
						}
					}
				}
			}
		}
		if (f != null) {
			addForward(str, f.path(), f.web(), f.method(), f.redirect(), indent, s, null);
		} else {
			GlobalForward gf = s.getGlobals().getForward(defF);
			if (gf != null) {
				addForward(str, gf.path(), gf.web(), gf.method(), gf.redirect(), indent, s, null);
			} else {
				LOG.warn("Cannot find forward called \"" + defF + "\"");
			}
		}
	}
	
	protected void addForward(StringBuffer str, String path, Class<?> web, String method, boolean redirect, String indent, Site s, String retVal) {
		if (redirect && path != null && (!path.equals(""))) {
			str.append(indent).append("response.sendRedirect(\"").append(path).append("\");\n");
		} else {
			if (web != null && web != Forward.class && web != GlobalForward.class) {
				Method forwardMethod = null;
				for (Method m : web.getMethods()) {
					if (m.getName().equals(method)) {
						forwardMethod = m;
						break;
					}
				}
				if (forwardMethod != null) {
					ActionHolder ah = s.getGlobals().getAction(forwardMethod);
					if (ah != null) {
						if (ah.getSite() == s) {
							str.append(indent).append("process").append(ah.getMethod().getName()).append("(request, response, servletContext);\n");
						} else {
							str.append(indent).append("((Site").append(ah.getSite().getName()).append(")getCache(\"Site").append(ah.getSite().getName()).append("\")).process").append(ah.getMethod().getName()).append(
							        "(request, response, servletContext);\n");
						}
					}
				}
			} else {
				str.append(indent).append("RequestDispatcher rd = servletContext.getRequestDispatcher(\"").append(path).append("\");\n");
				str.append(indent).append("rd.forward(request,response);\n");
			}
		}
		addReturn(str, indent, retVal);
	}
	
	private void addReturn(StringBuffer str, String indent, String retVal) {
		if (retVal == null) {
			str.append(indent).append("return;\n");
		} else {
			str.append(indent).append("return ").append(retVal).append(";\n");
		}
	}
	
	protected String buildMethodSigniture(Method method, Required required, boolean addName, Globals globals) {
		StringBuffer str = new StringBuffer();
		int paramPos = -1;
		boolean firstParam = true;
		Annotation[][] allAnnotations = method.getParameterAnnotations();
		String defaultName = null;
		if (method.getName().toLowerCase().startsWith("set")) {
			defaultName = method.getName().substring(3);
			defaultName = defaultName.substring(0, 1).toLowerCase() + defaultName.substring(1);
		}
		for (Class<?> c : method.getParameterTypes()) {
			paramPos++;
			firstParam = addParam(method, str, firstParam, paramPos, allAnnotations, c, required, defaultName, addName, globals);
		}
		return str.toString();
	}
	
	private boolean addParam(Method m, StringBuffer str, boolean firstParam, int paramPos, Annotation[][] allAnnotations, Class<?> c, Required required, String defaultName, boolean addName, Globals globals) {
		if (firstParam) {
			firstParam = false;
		} else {
			str.append(",");
		}
		if (c == HttpServletRequest.class) {
			str.append("request");
		} else if (c == HttpServletResponse.class) {
			str.append("response");
		} else if (c == Messages.class) {
			str.append("messages");
		} else if (isException(c)) {
			if (required == null) {
				str.append("getException(\"exception\", request,false, null,null,").append(c.getCanonicalName()).append(".class)");
			} else {
				str.append("getException(\"exception\", request,false, ").append(required.key()).append("\",\"").append(required.bundle()).append("\",").append(c.getCanonicalName()).append(".class)");
			}
		} else {
			String paramName = null;
			boolean clean = false;
			MethodTypeEnum method = MethodTypeEnum.getByClass(c);
			DefaultCacheValue defCache = null;
			DefaultValue defValue = null;
			Inject inject = null;
			for (Annotation annotation : allAnnotations[paramPos]) {
				if (annotation instanceof Parameter) {
					paramName = ((Parameter) annotation).value();
				} else if (annotation instanceof Clean) {
					clean = true;
				} else if (annotation instanceof DefaultCacheValue) {
					defCache = (DefaultCacheValue) annotation;
				} else if (annotation instanceof DefaultValue) {
					defValue = (DefaultValue) annotation;
				} else if (annotation instanceof Inject) {
					inject = (Inject) annotation;
				}
			}
			if (paramName == null) {
				paramName = defaultName;
			}
			if (method == null) {
				if (c.getAnnotation(Form.class) != null || defCache != null || defValue != null || inject != null || c.getAnnotation(MultipleInjection.class) != null || c.getAnnotation(Injectable.class) != null) {
					processForm(m, str, c, paramName, clean, defCache, defValue, inject, globals);
				} else {
					str.append("null");
				}
			} else {
				if (paramName == null && defCache == null && defValue == null) {
					str.append("null");
				} else {
					str.append(method.getMethod());
					if (addName) {
						str.append("(name+\"");
					} else {
						str.append("(\"");
					}
					str.append(paramName).append("\",request,");
					if (required == null) {
						str.append("false,null,null");
					} else {
						str.append("true,\"").append(required.key()).append("\",\"").append(required.bundle()).append("\"");
					}
					if (defCache != null) {
						str.append("getCache").append(method.toString()).append("\"").append(defCache.cache().cacheId()).append("\", \"").append(defCache.cache().id()).append("\",\"").append(defCache.value()).append("\",").append(
						        c.getClass().getCanonicalName()).append(".class, request)");
					} else if (defValue != null) {
						str.append("getDefault").append(method.toString()).append("(\"").append(defValue.value()).append("\",").append(c.getClass().getCanonicalName()).append(".class, request)");
					}
					str.append(")");
				}
			}
		}
		return firstParam;
	}
	
	private boolean isException(Class<?> c) {
		if (Throwable.class.isAssignableFrom(c)) {
			return true;
		}
		return false;
	}
	
	private boolean processForm(Method m, StringBuffer str, Class<?> c, String name, boolean clean, DefaultCacheValue defCache, DefaultValue defValue, Inject inject, Globals globals) {
		String helper = FormBuilder.createHelper(c);
		FormData fd = globals.getForm(c.getCanonicalName());
		if (fd == null) {
			globals.addForm(c);
			fd = globals.getForm(c.getCanonicalName());
		}
		if (defCache != null) {
			fd.addDefaultCache(m.getDeclaringClass().getCanonicalName() + ":" + m.getName(), defCache);
		}
		if (defValue != null) {
			fd.addDefaultValue(m.getDeclaringClass().getCanonicalName() + ":" + m.getName(), defValue);
		}
		if (inject != null) {
			fd.addInjects(m.getDeclaringClass().getCanonicalName() + ":" + m.getName(), inject);
		}
		boolean done = false;
		str.append(helper).append(".getForm(request,");
		if (name == null) {
			str.append("null, ");
		} else {
			str.append("\"").append(name).append("\", ").append(clean);
		}
		str.append(clean);
		String id = null;
		if (inject != null) {
			id = m.getDeclaringClass().getCanonicalName() + ":" + m.getName();
		} else if (defCache != null) {
			id = defCache.id();
		} else if (defValue != null) {
			id = defValue.id();
		}
		if (id == null || id.equals("")) {
			id = m.getDeclaringClass().getCanonicalName() + ":" + m.getName();
		}
		str.append(",\"").append(id).append("\")");
		done = true;
		return done;
	}
	
	private StringBuffer addClassStart(Site s) {
		StringBuffer str = new StringBuffer("import javax.servlet.http.*;\n");
		str.append("import javax.servlet.*;\n");
		str.append("import java.util.*;\n");
		str.append("import org.apache.log4j.Logger;\n");
		str.append("public class ").append("Site" + removeSpace(s.getName())).append(" extends uk.co.taylorconsulting.web.controller.WebHelper implements uk.co.taylorconsulting.web.controller.WebFramework {\n");
		ActionHolder defaultAction = null;
		Method defaultMethod = s.getDefaultAction();
		str.append("\tprivate static final Logger LOG = Logger.getLogger(Site").append(removeSpace(s.getName())).append(".class);\n");
		str.append("\tprivate static final ").append("Site" + removeSpace(s.getName())).append(" instance = new ").append("Site" + removeSpace(s.getName())).append("();\n");
		str.append("\tpublic ").append("Site" + removeSpace(s.getName())).append("(){}\n");
		str.append("\tpublic static ").append("Site" + removeSpace(s.getName())).append(" getInstance() {\n");
		str.append("\t\treturn instance;\n");
		str.append("\t}\n");
		if (s.getActions().size() == 0) {
			str.append("\tpublic void process(HttpServletRequest request, HttpServletResponse response, ServletContext servletContext) {\n");
			str.append("\t\tLOG.warn(\"Cannot process request. Action not defined for path: \"+request.getServletPath());\n");
		} else {
			str.append("\tprivate enum Actions {\n");
			str.append("\t\t");
			boolean first = true;
			for (ActionHolder ah : s.getActions().values()) {
				if (first) {
					first = false;
				} else {
					str.append(",");
				}
				str.append(ah.getAction().dispatch()).append("_").append(ah.getAction().value());
			}
			str.append(";\n");
			str.append("\t}\n");
			str.append("\tpublic void process(HttpServletRequest request, HttpServletResponse response, ServletContext servletContext) {\n");
			first = true;
			ArrayList<String> dispatches = new ArrayList<String>();
			first = true;
			for (ActionHolder ah : s.getActions().values()) {
				if (!dispatches.contains(ah.getAction().dispatch())) {
					dispatches.add(ah.getAction().dispatch());
					if (first) {
						first = false;
						str.append("\t\ttry {\n");
						str.append("\t\tLOG.debug(\"dispatcher=\"+request.getParameter(\"").append(ah.getAction().dispatch()).append("\"));\n");
						str.append("\t\tActions dispatcher = Actions.valueOf(\"").append(ah.getAction().dispatch()).append("_\"+request.getParameter(\"").append(ah.getAction().dispatch()).append("\"));\n");
					} else {
						str.append("\t\tif (dispatcher == null) {\n");
						str.append("\t\tLOG.debug(\"dispatcher=\"+request.getParameter(\"").append(ah.getAction().dispatch()).append("\"));\n");
						str.append("\t\t\tdispatcher = Actions.valueOf(\"").append(ah.getAction().dispatch()).append("_\"+request.getParameter(\"").append(ah.getAction().dispatch()).append("\");\n");
						str.append("\t\t}\n");
					}
				}
			}
			if (s.getActions().size() == 1) {
				ActionHolder ah = s.getActions().values().iterator().next();
				if (ah == null) {
					LOG.warn("no actions defined for site " + s.getName());
				}
				if (ah.getMethod() == defaultMethod) {
					str.append("\t\t} catch (Throwable t){t.printStackTrace();}\n");
					str.append("\t\tprocess").append(ah.getMethod().getName()).append("(request, response, servletContext);\n");
				} else {
					str.append("\t\tif (dispatcher == Action.").append(ah.getAction().dispatch()).append("_").append(ah.getAction().value()).append(") {\n");
					str.append("\t\t\tprocess").append(ah.getMethod().getName()).append("(request, response, servletContext);\n");
					str.append("\t\t}\n");
					str.append("\t\t} catch (Throwable t){t.printStackTrace();}\n");
				}
			} else {
				str.append("\t\tswitch(dispatcher) {\n");
				for (ActionHolder ah : s.getActions().values()) {
					if (ah.getMethod() == defaultMethod || ah.getAction().value().equals("xxdefaultxx")) {
						if (defaultAction == null) {
							defaultAction = ah;
						}
					} else {
						addMethodCall(str, ah, false);
					}
				}
				str.append("\t\t}\n");
				str.append("\t\t} catch (Throwable t){t.printStackTrace();}\n");
				if (defaultAction != null) {
					addMethodCall(str, defaultAction, true);
				} else {
					str.append("\t\tLOG.warn(\"Cannot process request. Action not defined for path: \"+request.getServletPath());\n");
				}
			}
		}
		str.append("\t}\n");
		return str;
	}
	
	private void addMethodCall(StringBuffer str, ActionHolder ah, boolean other) {
		if (!other) {
			str.append("\t\t\tcase ").append(ah.getAction().dispatch()).append("_").append(ah.getAction().value()).append(": {\n");
		}
		str.append("\t\t\t\tprocess").append(ah.getMethod().getName()).append("(request, response, servletContext);\n");
		if (!other) {
			str.append("\t\t\t\treturn;\n");
			str.append("\t\t\t}\n");
		}
	}
	
	private String removeSpace(String name) {
		name = name.trim();
		int i = name.indexOf(' ');
		while (i > 0) {
			name = name.substring(0, i).trim() + name.substring(i + 1).trim();
			i = name.indexOf(' ');
		}
		return name;
	}
}
